<script setup lang="ts">
import Button from '@/components/Button.vue' // 导入自定义按钮组件
import { generatAnalysis, generatTranslate, generateText } from '@/server/api' // 导入 API 请求方法
import { base64ToBlob, blobToBase64, verifyOpenKey } from '@/utils' // 导入工具函数
import { useConversationStore } from '@/stores' // 导入会话状态管理 Store

interface Translates { // 定义翻译结果的接口
  [key: string]: {
    isShow: boolean // 是否显示翻译结果
    result: string | boolean // 翻译结果
  }
}

// hooks
const store = useConversationStore() // 使用会话状态管理 Store
const { el, scrollToBottom } = useScroll() // 使用滚动 Hook，获取元素和滚动到底部的方法
const { selfAvatar, openKey, chatRememberCount, autoPlay } = useGlobalSetting() // 使用全局设置 Hook，获取用户头像、API Key、聊天记忆数量和自动播放设置

const {
  language,
  voiceName,
  rate,
  style,
  isRecognizing,
  isPlaying,
  startRecognizeSpeech,
  isRecognizReadying,
  stopRecognizeSpeech,
  ssmlToSpeak,
  isSynthesizing,
  audioBlob,
  player,
} = useSpeechService({ langs: store.allLanguage as any, isFetchAllVoice: false }) // 使用语音服务 Hook，获取语音相关状态和方法

// states
const message = ref('') // input message，用户输入的消息
const text = ref('') // current select message，当前选中的消息文本
const translates = ref<Translates>({}) // translate result，翻译结果
const analysisResults = ref<Translates>({}) // analysis result，分析结果
const isTranslating = ref(false) // translate loading，是否正在翻译
const analysisLoading = ref(false) // analysis loading，是否正在分析

const speakIndex = ref(0) // record speak，记录当前正在播放语音的消息索引
const translateIndex = ref(0) // record translate，记录当前正在翻译的消息索引
const analysisIndex = ref(0) // record analysis，记录当前正在分析的消息索引

const messageLength = computed(() => store.getConversationsByCurrentProps('chatMessages').length) // 计算消息长度
const chatMessages = computed(() => store.getConversationsByCurrentProps('chatMessages').slice(1))// 除去第一条系统设置的消息，获取聊天消息列表
const currentChatMessages = computed(() => store.getConversationsByCurrentProps('chatMessages')) // 获取当前聊天消息列表
const currentKey = computed(() => store.currentKey) // 获取当前 Key
const currentName = computed(() => store.getConversationsByCurrentProps('name')) // 获取当前会话名称
const currentAvatar = computed(() => store.getConversationsByCurrentProps('avatar')) // 获取当前会话头像
const currentLanguage = computed(() => store.getConversationsByCurrentProps('language')) // 获取当前会话语言
const currentVoice = computed(() => store.getConversationsByCurrentProps('voice')) // 获取当前会话语音
const currentRate = computed(() => store.getConversationsByCurrentProps('rate')) // 获取当前会话语速
const currentVoiceStyle = computed(() => store.getConversationsByCurrentProps('voiceStyle')) // 获取当前会话语音风格

useTitle(currentName) // 设置页面标题为当前会话名称

// 设置空格快捷键
useEventListener(document, 'keydown', (e) => {
  if (store.loading || isRecognizing.value || isRecognizReadying.value || e.code !== 'Space' || !store.isMainActive) return // 如果正在加载、识别中、准备识别中、按键不是空格、不在主活动状态，则不执行
  message.value = '' // 清空消息
  stopAllSpeaker() // 开启语音识别时停止所有语音播放
  startRecognizeSpeech((textSlice) => { // 开始语音识别
    message.value += textSlice || '' // 将识别到的文本添加到消息中
  })
})

useEventListener(document, 'keyup', async (e) => {
  if ((!isRecognizing.value && !isRecognizReadying.value) || e.code !== 'Space' || store.loading || !store.isMainActive) return // 如果不在识别中、不在准备识别中、按键不是空格、正在加载、不在主活动状态，则不执行
  console.log('stop')
  await stopRecognizeSpeech() // 停止语音识别
  onSubmit(true) // 提交消息，并标记为来自语音识别
})

onMounted(() => {
  setTimeout(() => {
    scrollToBottom() // 滚动到底部
  }, 1000)
})

// effects
watch(messageLength, () => nextTick(() => scrollToBottom())) // 监听消息长度，当消息长度变化时，滚动到底部
watch(currentKey, () => { // 监听当前 Key，当 Key 变化时，更新语音相关设置
  isPlaying.value = false // 停止播放
  language.value = currentLanguage.value as any // 更新语言
  voiceName.value = currentVoice.value // 更新语音
  rate.value = currentRate.value // 更新语速
  style.value = currentVoiceStyle.value // 更新语音风格
  nextTick(scrollToBottom) // 滚动到底部
}, {
  immediate: true, // 立即执行
})

enum FetchType { // 定义请求类型枚举
  chat = 'chat', // 聊天
  translate = 'translate', // 翻译
  analysis = 'analysis' // 分析
}
// methods
const fetchResponse = async (prompt: ChatMessage[] | string, type: FetchType = FetchType.chat) => { // 封装请求方法
  let content // 返回的内容
  try {
    let result // 请求结果
    if (type === FetchType.translate) { // 翻译
      result = await generatTranslate(prompt as string) as any // 调用翻译 API
    }
    else if (type === FetchType.chat) { // chat
      result = await generateText(prompt as ChatMessage[]) as any // 调用聊天 API
    }
    else if (type === FetchType.analysis) { // analysis
      result = await generatAnalysis(prompt as string) as any // 调用分析 API
    }

    if (result.error) alert(result.error?.message) // 如果有错误，则弹出错误信息
    else if (result?.object === 'error') alert(result?.message) // 兼容 api2d，如果有错误，则弹出错误信息
    else content = result.choices[0].message.content // 获取返回的内容
  }
  catch (error: any) { // 捕获错误
    if (error.code === 20)
      alert('[Error] 请求超时，请检查网络连接或代理') // 如果是请求超时，则弹出错误信息
    else
      alert(error.message) // 否则弹出错误信息
  }
  return content // 返回内容
}

async function onSubmit(fromRecognize = false) { // 提交消息
  if (!verifyOpenKey(openKey.value)) return alert('请输入正确的API-KEY') // 验证 API Key
  if (!message.value) return // 如果消息为空，则不提交

  store.changeConversations([ // 更新会话列表
    ...currentChatMessages.value,
    { content: message.value, role: 'user', audioBlob: fromRecognize ? await blobToBase64(audioBlob.value) : '' }, // 添加用户消息，如果来自语音识别，则添加语音 Blob
  ])
  const tempCurrentChatMessages = chatMessages.value.map(x => ({ content: x.content, role: x.role })) // 发送的请求中需去除audioBlob，去除 audioBlob
  const systemMessage = currentChatMessages.value[0] // 获取系统消息
  const relativeMessage = tempCurrentChatMessages.slice(-(Number(chatRememberCount.value))) // 保留最近的几条消息，获取相关消息
  const prompts = [systemMessage, ...relativeMessage] as ChatMessage[] // 构造 prompts

  message.value = '' // 清空消息
  store.changeLoading(true) // 设置加载状态

  const content = await fetchResponse(prompts) // 请求 API

  if (content) { // 如果有返回内容
    store.changeConversations([ // 更新会话列表
      ...currentChatMessages.value,
      {
        content, role: 'assistant', // 添加 AI 消息
      },
    ])
    if (autoPlay.value) // 如果开启了自动播放
      speakByAI(content, chatMessages.value.length - 1) // 播放 AI 语音
  }
  else {
    store.changeConversations(currentChatMessages.value.slice(0, -1)) // 移除最后一条消息
  }

  store.changeLoading(false) // 关闭加载状态
}

// user speak
let audio = new Audio() // 创建 Audio 对象

// 停止AI的语音播放
function stopUserSpeaker() { // 停止用户语音播放
  audio.pause() // 暂停播放
  audio.currentTime = 0 // 重置播放时间
  isPlaying.value = false // 设置播放状态
}

// 停止User的语音播放
function stopAISpeaker() { // 停止 AI 语音播放
  player.value.pause() // 暂停播放
  isPlaying.value = false // 设置播放状态
  isSynthesizing.value = false // 设置合成状态
}

function stopAllSpeaker() { // 停止所有语音播放
  stopUserSpeaker() // 停止用户语音播放
  stopAISpeaker() // 停止 AI 语音播放
}

// user speak
function speakByUser(audioData: string, index: number) { // 播放用户语音
  if (isPlaying.value || isSynthesizing.value) { // 如果正在播放或合成
    stopAllSpeaker() // 停止所有语音播放
    if (speakIndex.value === index) return // 点击同一个按钮,则不再重复播放
  }
  speakIndex.value = index // 记录当前播放的索引
  audio = new Audio(URL.createObjectURL(base64ToBlob(audioData))) // 创建 Audio 对象
  audio.play() // 播放
  audio.onplay = () => { // 播放时
    isPlaying.value = true // 设置播放状态
  }

  audio.onended = () => { // 播放结束时
    isPlaying.value = false // 设置播放状态
    speakIndex.value = -1 // 重置播放索引
  }
}

// assistant speak
function speakByAI(content: string, index: number) { // 播放 AI 语音
  if (isPlaying.value || isSynthesizing.value) { // 如果正在播放或合成
    stopAllSpeaker() // 停止所有语音播放
    if (speakIndex.value === index) return // 点击同一个按钮,则不再重复播放
  }
  speakIndex.value = index // 记录当前播放的索引
  text.value = content // 设置当前文本
  ssmlToSpeak(content) // 合成语音并播放
}

const recognize = async () => { // 语音识别
  try {
    console.log('isRecognizing', isRecognizing.value)
    if (isRecognizing.value) { // 如果正在识别
      await stopRecognizeSpeech() // 停止语音识别
      onSubmit(true) // 提交消息，并标记为来自语音识别
      console.log('submit', message.value)
      return
    }
    message.value = '' // 清空消息

    stopAllSpeaker() // 开启语音识别时停止所有语音播放
    startRecognizeSpeech((textSlice) => { // 开始语音识别
      message.value += textSlice || '' // 将识别到的文本添加到消息中
    })
  }
  catch (error) { // 捕获错误
    alert(error) // 弹出错误信息
  }
}

const translate = async (text: string, i: number) => { // 翻译
  translateIndex.value = i // 记录当前翻译的索引
  const key = text + i // 生成 key
  if (translates.value[key]) // 如果已经翻译过
    return translates.value[key].isShow = !translates.value[key].isShow // 显示/隐藏翻译结果

  isTranslating.value = true // 设置翻译状态

  const content = await fetchResponse(text, FetchType.translate) // 请求翻译 API

  if (!content) return (isTranslating.value = false) // 如果没有返回内容，则关闭翻译状态

  translates.value = { // 更新翻译结果
    ...translates.value,
    [key]: { result: content, isShow: true },
  }
  isTranslating.value = false // 关闭翻译状态
}

async function grammarAnalysis(text: string, i: number) { // 语法分析
  analysisIndex.value = i // 记录当前分析的索引
  const key = text + i // 生成 key
  if (analysisResults.value[key]) // 如果已经分析过
    return analysisResults.value[key].isShow = !analysisResults.value[key].isShow // 显示/隐藏分析结果

  analysisLoading.value = true // 设置分析状态

  const content = await fetchResponse(text, FetchType.analysis) // 请求分析 API

  if (!content) return (analysisLoading.value = false) // 如果没有返回内容，则关闭分析状态

  analysisResults.value = { // 更新分析结果
    ...analysisResults.value,
    [key]: { result: content, isShow: true },
  }
  analysisLoading.value = false // 关闭分析状态
}
</script>

<template>
  <div flex flex-col p-2 rounded-md bg-white dark="bg-#1e1e1e" shadow-sm>
    <div ref="el" class="hide-scrollbar flex-1 overflow-auto">
      <template v-if="chatMessages.length">
        <div v-for="item, i in chatMessages" :key="i" center-y
          :class="item.role === 'user' ? 'flex-row-reverse w-[75%] ml-[25%]' : 'w-[75%]'">
          <div class="w-10 h-10">
            <img w-full h-full object-fill rounded-full :src="item.role === 'user' ? selfAvatar : currentAvatar" alt="">
          </div>
          <div style="flex-basis:fit-content" mx-2>
            <p p-2 my-2 chat-box>
              {{ item.content }}
            </p>
            <p v-show="translates[item.content + i]?.isShow" p-2 my-2 chat-box>
              {{ translates[item.content + i]?.result }}
            </p>

            <p v-show="item.role === 'user' && analysisResults[item.content + i]?.isShow" p-2 my-2 chat-box>
              {{ analysisResults[item.content + i]?.result }}
            </p>

            <!-- assistant -->
            <p v-if="item.role === 'assistant'" mt-2 flex>
              <template v-if="speakIndex !== i">
                <span class="chat-btn" @click="speakByAI(item.content, i)">
                  <i icon-btn rotate-90 i-ic:sharp-wifi />
                </span>
              </template>
              <template v-else>
                <span v-if="isSynthesizing || isPlaying" class="chat-btn" @click="stopAISpeaker()">
                  <i icon-btn rotate-90 i-svg-spinners:wifi-fade />
                </span>
                <span v-else class="chat-btn" @click="speakByAI(item.content, i)">
                  <i icon-btn rotate-90 i-ic:sharp-wifi />
                </span>
              </template>
              <span v-if="!isTranslating || translateIndex !== i" ml-1 class="chat-btn"
                @click="translate(item.content, i)">
                <i icon-btn i-carbon:ibm-watson-language-translator />
              </span>
              <span v-else ml-1 class="chat-btn">
                <i icon-btn i-eos-icons:bubble-loading />
              </span>
            </p>

            <!-- user -->
            <p v-else justify-end mt-2 flex>
              <template v-if="item.audioBlob">
                <template v-if="speakIndex !== i">
                  <span class="chat-btn mr-1" @click="speakByUser(item.audioBlob, i)">
                    <i icon-btn rotate-270 i-ic:sharp-wifi />
                  </span>
                </template>
                <template v-else>
                  <span v-if="isPlaying" class="chat-btn mr-1" @click="stopUserSpeaker()">
                    <i icon-btn rotate-270 i-svg-spinners:wifi-fade />
                  </span>
                  <span v-else class="chat-btn mr-1" @click="speakByUser(item.audioBlob, i)">
                    <i icon-btn rotate-270 i-ic:sharp-wifi />
                  </span>
                </template>
              </template>
              <span v-if="!analysisLoading || analysisIndex !== i" class="chat-btn"
                @click="grammarAnalysis(item.content, i)">
                <i icon-btn i-ic:outline-lightbulb />
              </span>
              <span v-else class="chat-btn">
                <i icon-btn i-eos-icons:bubble-loading />
              </span>
            </p>
          </div>
        </div>
      </template>
      <template v-else>
        <div font-italic text-gray-500 center h-full>
          Haven't started the conversation yet, let's start now
        </div>
      </template>
    </div>

    <!-- 语音输入=>功能: 点击麦克风图标触发 recognize() 方法，开始语音识别。 -->
    <div class="flex h-10 w-[-webkit-fill-available] mt-1 items-center justify-between">
      <Button mr-1 :disabled="isRecognizReadying || store.loading" @click="recognize()">
        <i v-if="isRecognizReadying" i-eos-icons:bubble-loading />
        <i v-else i-carbon:microphone class="icon-large"/>
      </Button>

      <div v-if="isRecognizing" class="loading-btn input-box p-3 flex-1">
        识别中，请讲话
        <i icon-btn i-eos-icons:three-dots-loading />
      </div>
      <div v-else-if="isRecognizReadying" class="loading-btn input-box p-3 flex-1">
        录音设备准备中
        <i icon-btn i-eos-icons:three-dots-loading />
      </div>
        <!-- 输入框 -->
      <input v-else-if="!store.loading" v-model="message" type="text" placeholder="点击语音按钮开始您的对话..." 
      class="input-box p-3 flex-1" @blur="store.changeMainActive(true)" @focus="store.changeMainActive(false)"
        @keypress.enter="() => onSubmit()">
      <div v-else class="loading-btn input-box p-3 flex-1">
       AI 正在思考
        <i icon-btn i-eos-icons:three-dots-loading />
      </div>
      <!-- 功能: 点击发送图标触发 onSubmit() 方法，提交当前输入框中的消息。 -->
      <Button :disabled="store.loading" mx-1 @click="onSubmit">
        <i i-carbon:send-alt class="icon-large"/>
      </Button>
      <!--  功能: 点击垃圾桶图标触发 store.cleanCurrentConversations() 方法，清空当前会话内容。-->
      <Button :disabled="store.loading" @click="store.cleanCurrentConversations()">
        <i i-carbon:trash-can class="icon-large"/>
      </Button>
    </div>
  </div>
</template>

<style scoped>
/* 调整包含图标的 div 样式 */
.icon-container {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 64px; /* 调整高度 */
  width: 64px;  /* 调整宽度 */
}

/* 调整图标大小 */
.icon-large {
  font-size: 50px; /* 调整图标大小 */
}

/* 输入框样式 */
.input-box {
  height: 48px; /* 调整输入框高度 */
  font-size: 30px; /* 调整字体大小 */
}
</style>